package cn.yj.syslog.common;

import cn.yj.common.UUIdUtils;
import cn.yj.commons.utils.HttpHeaderUtil;
import cn.yj.commons.utils.StringUtils;
import cn.yj.syslog.entity.SysLog;
import cn.yj.syslog.filter.LogRequestWrapper;
import cn.yj.syslog.service.ISysLogService;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import org.aopalliance.intercept.MethodInvocation;
import org.apache.catalina.connector.RequestFacade;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.multipart.MultipartFile;

import javax.servlet.ServletRequest;
import javax.servlet.ServletRequestWrapper;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.xml.ws.RequestWrapper;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;

/**
 * @author 永健
 * @since 2022-01-08 15:46
 */
public class DefaultRecords implements Records {
    private final static Logger logger = LoggerFactory.getLogger(DefaultRecords.class);

    /**
     * 敏感词字段
     */
    private static final Set<String> fields;

    static {
        fields = new HashSet<>();
        fields.add("password");
        fields.add("newPassword");
        fields.add("oldPassword");
    }

    private SysLog operaLog;
    private HttpServletRequest request;
    private ISysLogService iSysLogService;
    private OperateLog annotation;
    private long startTime;

    public DefaultRecords(HttpServletRequest request, ISysLogService iSysLogService) {
        this.operaLog = new SysLog(UUIdUtils.getUUId32());
        this.request = request;
        this.iSysLogService = iSysLogService;
        this.startTime = System.currentTimeMillis();
    }

    public HttpServletRequest getRequest() {
        return this.request;
    }

    @Override
    public void before(MethodInvocation methodInvocation) {
        Method method;
        Map<String, String> stringStringHashMap;

        method = methodInvocation.getMethod();
        this.annotation = method.getAnnotation(OperateLog.class);

        operaLog.setUrl(request.getRequestURI());

        String className = method.getDeclaringClass().getSimpleName();
        String methodName = method.getName();
        String type = request.getMethod();
        operaLog.setMethod(className + "/" + methodName + "()/" + type);

        this.request.setAttribute("logRecords", this);

        stringStringHashMap = new HashMap<>();
        stringStringHashMap.put("remark", "");

        // request 请求参数
        try {
            Map<String, String[]> parameterMap = request.getParameterMap();
            operaLog.setParams(operaLog.getMapToParams(parameterMap));
        } catch (Exception e) {
            e.printStackTrace();
            stringStringHashMap.put("remark", "解析请求参数异常：" + e.getMessage());
            operaLog.setParams(JSON.toJSONString(stringStringHashMap));
        }


        try {

            // 方法接收到的参数
            String reqParams = getReqParams(methodInvocation.getArguments(), method.getParameterTypes(),
                    methodInvocation.getMethod().getParameters());
            operaLog.setMethodParams(reqParams);

        } catch (Exception exception) {
            exception.printStackTrace();
            stringStringHashMap.put("remark", "解析方法参数异常：" + exception.getMessage());
            logger.error("解析方法参数异常,{}", exception.getMessage());
            operaLog.setMethodParams(JSON.toJSONString(stringStringHashMap));
        }

        try {
            // body参数
            LogRequestWrapper logRequestWrapper;
            if (request instanceof LogRequestWrapper) {
                logRequestWrapper = (LogRequestWrapper) request;
                String bodyString = logRequestWrapper.getBody();
                if (!StringUtils.isBlank(bodyString)) {
                    bodyString = FilterSensitiveWords(JSON.parseObject(bodyString, Map.class));
                }
                operaLog.setRequestBodyParams(bodyString);
            } else {
                if (request instanceof ServletRequestWrapper) {
                    ServletRequest request = ((ServletRequestWrapper) this.request).getRequest();
                    if (request instanceof LogRequestWrapper) {
                        logRequestWrapper = (LogRequestWrapper) request;
                        String bodyString = logRequestWrapper.getBody();
                        bodyString = FilterSensitiveWords(JSON.parseObject(bodyString, Map.class));
                        operaLog.setRequestBodyParams(bodyString);
                    }
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
            logger.error("解析body参数异常");
            stringStringHashMap.put("remark", "解析方法参数异常：" + e.getMessage());
            operaLog.setRequestBodyParams(JSON.toJSONString(stringStringHashMap));
        }
    }

    @Override
    public void after(Object result) {
        operaLog.setResult(result != null? JSON.toJSONString(result) : "");

        Object user = request.getAttribute("user");
        operaLog.setOperUser(user != null? user.toString() : "");
        Object device = request.getAttribute("device");
        operaLog.setSourceDevice(device != null? device.toString() : "未知");
        operaLog.setContentType(request.getContentType());

        if (StringUtils.isBlank(operaLog.getType())) {
            operaLog.setType("正常");
        }
        operaLog.setCreateTime(new Date());
        operaLog.setUpdateTime(operaLog.getCreateTime());

        operaLog.setOperateType(annotation != null? annotation.type().name() : LogType.OTHER.name());

        operaLog.setDescr(annotation != null? annotation.describe() : "");
        operaLog.setOperateType(annotation != null? annotation.type().name() : "");

        operaLog.setTakeTime(System.currentTimeMillis() - this.startTime);

        operaLog.setHost(HttpHeaderUtil.getIpAddr(request));
        String userAgent = HttpHeaderUtil.getUserAgent(request);
        operaLog.setSys(HttpHeaderUtil.getSys(userAgent));
        operaLog.setBrowser(HttpHeaderUtil.getBrowser(userAgent));
        // ip所在地
        operaLog.setIpAddress(HttpHeaderUtil.getAddress(operaLog.getHost()));


        logger.info("##### 请求日志\n {}", operaLog);
        try {
            if (iSysLogService != null && annotation != null) {
                iSysLogService.insertOrUpdate(this.operaLog);
            }
        } catch (Exception e) {
            e.printStackTrace();
            logger.error("插入日志记录异常：{}", e.getMessage());
        }
    }

    @Override
    public void error(Throwable e) {
        try {
            if (e != null) {
                this.request.setAttribute("logRecords", this);
                operaLog.setErrorMsg(e.getMessage());
                StringBuffer errMsg = new StringBuffer();
                if (e.getStackTrace().length > 0) {
                    operaLog.setType("异常");
                    StackTraceElement stackTraceElement = e.getStackTrace()[0];
                    int lineNumber = stackTraceElement.getLineNumber();
                    String fileName = stackTraceElement.getFileName();
                    errMsg.append("<p>").append("url:").append(operaLog.getUrl()).append("</p>");
                    errMsg.append("<p>").append("参数:").append(operaLog.getParams()).append("</p>");
                    errMsg.append("<p>").append("方法:").append(operaLog.getMethod()).append("</p>");
                    errMsg.append("<p>").append("错误文件:").append(fileName).append("->").append(lineNumber).append("行 " + "出现错误");
                    errMsg.append("<p>").append("错误信息:").append(e.getMessage());
                    operaLog.setErrorMsg(errMsg.toString());

                    if (iSysLogService != null && annotation != null) {
                        iSysLogService.insertOrUpdate(this.operaLog);
                    }
                }
            }
        } catch (Exception exception) {
            exception.printStackTrace();
        }
    }


    public void errorResult(HttpServletRequest request, String errorResult) {
        this.operaLog.setResult(errorResult);
        iSysLogService.insertOrUpdate(this.operaLog);
    }

    private String getReqParams(
            final Object[] inParams, final Class<?>[] parameterTypes, final Parameter[] parameters) {
        Object[] params = new Object[inParams.length];
        Object[] tempParams = new Object[inParams.length];
        for (int i = 0; i < inParams.length; i++) {
            params[i] = inParams[i];
        }

        for (int i = 0; i < parameterTypes.length; i++) {
            HashMap<String, Object> map = new HashMap<>();
            Object paramValue = null;
            try {
                paramValue = params[i];
            } catch (ArrayIndexOutOfBoundsException e) {
            }

            Class<?> parameterType = parameterTypes[i];

            String keyName = parameters[i].getName();

            String parameterTypeName = parameterType.getName();
            tempParams[i] = map;
            if ((HttpServletRequest.class == parameterType)) {
                map.put(keyName, "HttpServletRequest.class");
                continue;
            }

            if (HttpServletResponse.class == parameterType) {
                map.put(keyName, "HttpServletResponse.class");

                continue;
            }

            if (isBasicDataType(parameterType)) {
                if (fields.contains(parameterTypeName)) {
                    paramValue = "******";
                }
                map.put(keyName, paramValue);
                continue;
            }

            if (parameterType == MultipartFile.class) {
                map.put(keyName, ((MultipartFile) paramValue).getOriginalFilename());

                continue;
            }


            Class<?> aClass = paramValue.getClass();
            if (aClass == HashMap.class || aClass == ConcurrentHashMap.class || aClass == LinkedHashMap.class) {
                Map mv = (Map) paramValue;
                mv.forEach((k, v) -> {
                    if (fields.contains(k)) {
                        mv.put(k, "*****");
                    }
                });
                paramValue = mv;
            } else {
                // 当作javaBean对象去解析
                try {
                    Field[] declaredFields = aClass.getSuperclass().getDeclaredFields();
                    Field[] allFields = aClass.getDeclaredFields();
                    Map<String, Field> fieldsMap = Arrays.stream(allFields).collect(Collectors.toMap(Field::getName,
                            field -> field));
                    Map<String, Field> fieldsMap2 =
                            Arrays.stream(declaredFields).collect(Collectors.toMap(Field::getName, field -> field));
                    fieldsMap.putAll(fieldsMap2);
                    Object finalParamValue = paramValue;
                    fieldsMap.forEach((k, v) -> {
                        if (fields.contains(k)) {
                            v.setAccessible(true);
                            try {
                                v.set(finalParamValue, "******");
                            } catch (IllegalAccessException e) {
                                e.printStackTrace();
                            }
                        }
                    });
                } catch (SecurityException e) {
                    //e.printStackTrace();
                }
            }
            map.put(parameterTypeName, paramValue);
            tempParams[i] = map;
        }
        String mParams = JSON.toJSONString(tempParams);
        return mParams;
    }

    private String FilterSensitiveWords(Map<String, Object> map) {
        for (Map.Entry<String, ?> entry : map.entrySet()) {
            String k = entry.getKey();
            // 敏感词过滤
            if (fields.contains(k)) {
                map.put(k, "******");
            }
        }
        return JSON.toJSONString(map);
    }


    private final static Set<Class<?>> BASI_DATA_TYPE = new HashSet<>();

    static {
        BASI_DATA_TYPE.add(String.class);
        BASI_DATA_TYPE.add(Integer.class);
        BASI_DATA_TYPE.add(Long.class);
        BASI_DATA_TYPE.add(Float.class);
        BASI_DATA_TYPE.add(Boolean.class);
        BASI_DATA_TYPE.add(Double.class);
        BASI_DATA_TYPE.add(Short.class);
        BASI_DATA_TYPE.add(Byte.class);
    }

    private boolean isBasicDataType(Class<?> type) {
        return BASI_DATA_TYPE.contains(type);
    }
}
